<?php
declare(strict_types=1);

namespace app\service;

use support\Redis;

/**
 * Jt808Decode
 * 标准 JT/T 808 解码流程：
 * 1) 校验：0x7E...0x7E 框架 → 反转义 → 异或校验（最后 1 字节）；
 * 2) 解析头部：msg_id(2) | body_props(2) | terminal_id(6 BCD) | serial(2) | [subpkg(4 可选)]；
 * 3) 提取消息体（长度 = body_props.len），如 0x0200（位置）；
 * 4) AES：仅当 env('AES_ENABLE')=1 才对“消息体”尝试 AES-128-ECB 解密（PKCS#7），失败则保留原体；
 *
 * 注意：
 * - 校验必须在解密前进行，否则误解析。
 * - 终端号使用 BCD 解码为字符串（一般 12~20 位，不固定长度）。
 */
class Jt808Decode
{
    private string $aesKey;
    private bool $aesEnable;

    public function __construct(string $aesKey)
    {
        $this->aesKey = $aesKey;
        $this->aesEnable = (bool)env('AES_ENABLE', false);
    }

    /**
     * 解码完整帧
     * @param string $raw 原始 UDP 数据
     * @return array{msg_id:int, body_props:int, terminal:string, serial:int, time?:string, body:string, valid:bool}
     */
    public function decode(string $raw): array
    {
        // 1. 检查起止位
        if (strlen($raw) < 5 || $raw[0] !== "\x7E" || substr($raw, -1) !== "\x7E") {
            throw new \RuntimeException('JT808 frame missing 0x7E delimiters');
        }

        // 2. 取中间内容并反转义
        $middle = substr($raw, 1, -1);
        $data = $this->unescape($middle);

        // 3. 校验（最后1字节为异或校验）
        if (strlen($data) < 5) {
            throw new \RuntimeException('JT808 frame too short');
        }
        $payload = substr($data, 0, -1);
        $checksum = ord(substr($data, -1));
        $calc = $this->xorChecksum($payload);
        if ($checksum !== $calc) {
            throw new \RuntimeException('JT808 checksum invalid');
        }

        // 4. 解析头部
        $off = 0;
        $msg_id = $this->u16($payload, $off); $off += 2;
        $body_props = $this->u16($payload, $off); $off += 2;
        $body_len = $body_props & 0x03FF; // 低10位为长度
        $encrypt_type = ($body_props >> 10) & 0x03; // 00 无加密, 01 RSA，其它预留（厂家扩展可能用于 AES）
        $terminal = $this->bcd(substr($payload, $off, 6)); $off += 6;
        $serial = $this->u16($payload, $off); $off += 2;

        // 分包判断
        $has_subpkg = (($body_props & 0x2000) !== 0);
        if ($has_subpkg) {
            // 总包数(2) + 序号(2)
            $off += 4;
        }

        if ($off + $body_len > strlen($payload)) {
            throw new \RuntimeException('JT808 body length overflow');
        }
        $body = substr($payload, $off, $body_len);

        // 5. AES（仅在配置开启时尝试）
        if ($this->aesEnable && !empty($this->aesKey)) {
            $dec = $this->aesDecryptStrict($body, $this->aesKey);
            if ($dec !== null) {
                $body = $dec;
            }
        }

        return [
            'msg_id' => $msg_id,
            'body_props' => $body_props,
            'terminal' => $terminal,
            'serial' => $serial,
            'body' => $body,
            'valid' => true,
        ];
    }

    /**
     * 解析 0x0200 位置上报
     * 标准体：
     * - 报警标志(4)
     * - 状态(4)
     * - 纬度(4) WGS84 × 10^6
     * - 经度(4) WGS84 × 10^6
     * - 高程(2) 米
     * - 速度(2) 1/10 km/h
     * - 方向(2) 0-359
     * - 时间(6 BCD) YYMMDDhhmmss
     * 可选附加项：略（不同厂商扩展）
     */
    public function parse0200(string $body): array
    {
        $off = 0;
        if (strlen($body) < 28) {
            throw new \RuntimeException('0200 body too short');
        }
        $alarm = $this->u32($body, $off); $off += 4;
        $status = $this->u32($body, $off); $off += 4;
        $lat = $this->u32($body, $off); $off += 4;
        $lon = $this->u32($body, $off); $off += 4;
        $alt = $this->u16($body, $off); $off += 2;
        $speed_raw = $this->u16($body, $off); $off += 2;
        $direction = $this->u16($body, $off); $off += 2;
        $time_bcd = substr($body, $off, 6); $off += 6;

        $speed_kmh = $speed_raw / 10.0;
        $gpstime = $this->bcdTime6($time_bcd);

        // 可选附加项解析留空（不同厂商偏移不一致）
        $acc = ($status & 0x0002) ? 1 : 0; // 示例：状态位解析（需按厂家文档调整）
        $located = ($status & 0x0001) ? 1 : 0;

        return [
            'alarm' => $alarm,
            'status' => $status,
            'lat' => $lat,
            'lon' => $lon,
            'alt' => $alt,
            'speed' => (int)round($speed_kmh),
            'direction' => $direction,
            'acc' => $acc,
            'located' => $located,
            'gpstime' => $gpstime,
        ];
    }

    private function unescape(string $data): string
    {
        // 0x7D 0x02 -> 0x7E；0x7D 0x01 -> 0x7D
        $out = '';
        $len = strlen($data);
        for ($i = 0; $i < $len; $i++) {
            $ch = $data[$i];
            if ($ch === "\x7D" && $i + 1 < $len) {
                $next = $data[$i + 1];
                if ($next === "\x02") {
                    $out .= "\x7E"; $i++;
                    continue;
                } elseif ($next === "\x01") {
                    $out .= "\x7D"; $i++;
                    continue;
                }
            }
            $out .= $ch;
        }
        return $out;
    }

    private function xorChecksum(string $data): int
    {
        $x = 0;
        $len = strlen($data);
        for ($i = 0; $i < $len; $i++) {
            $x ^= ord($data[$i]);
        }
        return $x & 0xFF;
    }

    private function aesDecryptStrict(string $data, string $key): ?string
    {
        // 仅尝试一次解密；返回 null 代表不使用解密结果
        $dec = openssl_decrypt($data, 'aes-128-ecb', $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING);
        if ($dec === false) {
            return null;
        }
        // 校验 PKCS#7 填充合法性
        $pad = ord(substr($dec, -1));
        if ($pad < 1 || $pad > 16) {
            return null;
        }
        // 验证末尾 pad 字节都相同
        $tail = substr($dec, -$pad);
        if ($tail !== str_repeat(chr($pad), $pad)) {
            return null;
        }
        return substr($dec, 0, -$pad);
    }

    private function u16(string $bin, int $off): int
    {
        $arr = unpack('n', substr($bin, $off, 2) ?: "\x00\x00");
        return (int)$arr[1];
    }

    private function u32(string $bin, int $off): int
    {
        $arr = unpack('N', substr($bin, $off, 4) ?: "\x00\x00\x00\x00");
        return (int)$arr[1];
    }

    private function bcd(string $bin): string
    {
        $out = '';
        for ($i = 0, $l = strlen($bin); $i < $l; $i++) {
            $byte = ord($bin[$i]);
            $hi = ($byte >> 4) & 0x0F;
            $lo = $byte & 0x0F;
            $out .= $hi . $lo;
        }
        return ltrim($out, '0');
    }

    private function bcdTime6(string $bin): string
    {
        if (strlen($bin) !== 6) return date('Y-m-d H:i:s');
        $digits = $this->bcd($bin);
        // YYMMDDhhmmss
        $yy = substr($digits, 0, 2);
        $mm = substr($digits, 2, 2);
        $dd = substr($digits, 4, 2);
        $hh = substr($digits, 6, 2);
        $mi = substr($digits, 8, 2);
        $ss = substr($digits, 10, 2);
        $year = (int)$yy + 2000;
        return sprintf('%04d-%02d-%02d %02d:%02d:%02d', $year, (int)$mm, (int)$dd, (int)$hh, (int)$mi, (int)$ss);
    }
}